import math
import re
from datetime import datetime

from markupsafe import Markup
from odoo import fields, models, api, _, Command
from odoo.exceptions import ValidationError
from odoo.tools import float_compare


class ShippingMethod(models.Model):
    _name = 's.shipping.method'
    _description = 'Shipping Method'

    name = fields.Char(string='Service Name', required=True)
    code = fields.Char(string='Service Code')
    # freight_collect_payment = fields.Boolean(default=False, string="Freight Collect Payment")
    # bank_num = fields.Char(string="Bank")


class SBPickingListDatecodeLine(models.Model):
    _name = 'sb.picking.list.datecode.line'
    _description = 'Datecode Line for Picking List'

    package_id = fields.Many2one('sb.picking.list', string="Package", ondelete="cascade")
    product_id = fields.Many2one('product.product', string='Product')
    version = fields.Char('Version')
    datecode = fields.Char(string="Date Code")
    qty = fields.Integer(string="Qty")

    @api.depends('datecode')
    def _compute_display_name(self):
        for line in self:
            # line.display_name = line.datecode + '/' + str(line.qty) if line.datecode else ''
            line.display_name = f"{line.datecode}/{line.qty or ''}"


class SbPickingListLine(models.Model):
    _name = 'sb.picking.list.line'

    picking_list_id = fields.Many2one('sb.picking.list', ondelete="cascade")
    quant_id = fields.Many2one('stock.quant', "Pick From")  # Dummy field for the detailed operation view
    quantity = fields.Float('Quantity', digits='Product Unit of Measure')
    available_quantity = fields.Float(related='quant_id.available_quantity')
    pd_id = fields.Many2one('sb.packing.delivery')
    product_id = fields.Many2one('product.product', string='Product')
    # 实际剩余数量
    act_available_quantity = fields.Float(compute='compute_quantity', string="Available Quantity", store=True)
    # 当前quant出货指示占用数量
    used_available_quantity = fields.Float(compute='compute_quantity', store=True)

    @api.model_create_multi
    def create(self, vals_list):
        res = super(SbPickingListLine, self).create(vals_list)
        for rec in res:
            rec.pd_id = rec.picking_list_id.pd_id.id
        return

    def write(self, vals):
        vals['pd_id'] = self.picking_list_id.pd_id.id
        res = super(SbPickingListLine, self).write(vals)
        return res

    @api.depends('quant_id', 'quantity')
    def compute_quantity(self):
        # 当前quant，出货指示的占用数量占用数量
        for rec in self:
            all_pds = self.env['sb.packing.delivery'].sudo().search([('state', 'in', ['pack', 'confirming']), ('id', '!=', rec.pd_id.id)])
            all_lines = rec.pd_id.picking_list_lines
            for pd in all_pds:
                all_lines |= pd.picking_list_lines
            used_available_quantity = sum([al.quantity for al in all_lines if
                                           al.product_id.id == rec.product_id.id
                                           and al.quant_id == rec.quant_id
                                           and rec.picking_list_id != al.picking_list_id])
            rec.used_available_quantity = used_available_quantity
            rec.act_available_quantity = rec.available_quantity - used_available_quantity

    @api.onchange('quant_id')
    def onchange_quantity(self):
        if not self.quant_id: return
        # self.pd_id = self.picking_list_id.pd_id.id
        # available_quantity = self._get_available_quantity()
        available_quantity = self.act_available_quantity
        remain_available_quantity = self.picking_list_id.remain_available_quantity
        # 库存可用>=剩余
        if available_quantity > 0 and available_quantity >= remain_available_quantity:
            self.quantity = remain_available_quantity
        # 库存可用 < 剩余
        elif available_quantity > 0 and available_quantity < remain_available_quantity:
            self.quantity = available_quantity

    # @api.onchange('quantity')
    # def onchange_check_qty(self):
    #     if not self.quantity: return
    #     # available_quantity = self._get_available_quantity()
    #     available_quantity = self.act_available_quantity
    #     if self.quantity > available_quantity:
    #         raise ValidationError(_('The maximum stock for the current batch cannot exceed'))


class SBPickingList(models.Model):
    _name = 'sb.picking.list'
    _description = "Picking List"

    name = fields.Char(string="ID", related='shipping_instructions_line_id.name')
    desc = fields.Char(string='PN', related='shipping_instructions_line_id.pn')
    pn_desc = fields.Char(string='PN Description', related='shipping_instructions_line_id.pn_desc')
    qty = fields.Integer(string='Quantity(PCS/BOX)')
    package = fields.Integer(string='Package(Box)', default=1)
    n_weight = fields.Float(string='N.W.(Box/kg)')
    g_weight = fields.Float(string='G.W.(Box/kg)', compute='_compute_g_weight', store=True, readonly=False)
    pcs_weight = fields.Float(string='PCS W.(g)')
    note = fields.Char(string='Date Code')
    shipping_instructions_id = fields.Many2one('ship.instructions', ondelete='cascade')
    shipping_instructions_line_id = fields.Many2one('ship.instructions.line', string="MO",
                                                    domain="[('ship_instructions_id', '=', shipping_instructions_id)]",
                                                    ondelete='cascade')
    delivery_qty = fields.Float(digits=(16, 0), string='Delivery Qty',
                                related='shipping_instructions_line_id.delivery_qty')
    product_id = fields.Many2one('product.product', string='Product',
                                 related='shipping_instructions_line_id.product_id')
    pd_id = fields.Many2one('sb.packing.delivery')
    state = fields.Selection(related='pd_id.state')
    po = fields.Char(string='PO', related='shipping_instructions_line_id.po_name')
    end_pn = fields.Char(string='End PN', related='shipping_instructions_line_id.end_pn')
    end_po = fields.Char(string='End PO', related='shipping_instructions_line_id.end_po')
    currency_id = fields.Many2one('res.currency', string='Currency',
                                  related='shipping_instructions_line_id.currency_field')
    unit_price = fields.Float(string='Unit Price', related='shipping_instructions_line_id.unit_price')
    box = fields.Many2one('stock.package.type', string='Box Dimension(mm)')
    ttl_qty = fields.Integer(string='TTL Qty', compute='_compute_ttl_package', store=True)
    ttl_value = fields.Monetary(string='TTL Value', compute='_compute_ttl_package', currency_field='currency_id',
                                store=True)
    tt_g_weight = fields.Float(string='TTL G.W.(kg)', compute='_compute_tt_g_weight')
    tt_n_weight = fields.Float(string='TTL N.W.(kg)', compute='_compute_tt_g_weight')
    picking_list_lines = fields.One2many('sb.picking.list.line', 'picking_list_id', string='Stock')
    select_qty = fields.Float('select qty', compute='_compute_select_qty', store=True)

    display_assign_serial = fields.Boolean(defualt=True)
    quant_ids = fields.Many2many('stock.quant', string='Domain Quant')
    lot_ids = fields.Many2many('stock.lot', string='Domain Lot')
    remain_available_quantity = fields.Float(string='Stock Quant Remain Available Qty')
    datecode_line_ids = fields.One2many('sb.picking.list.datecode.line', 'package_id', string="DateCode")
    mix_box_id_ids = fields.Many2many(
        'sb.wrapped.in.box',
        string='Wrapped In Box'
    )
    mix = fields.Boolean(string='Mix Package')
    display_mix = fields.Char(string='Mix Box', compute='_compute_mix_box_id_ids', store=True)
    free_sample = fields.Integer(string='Free Sample', default=0, help="Free sample quantity for this package")

    # @api.constrains('select_qty')
    # def constrains_date_code_qty(self):
    #     for rec in self:
    #         if float_compare(rec.select_qty, rec.ttl_qty + rec.free_sample, precision_rounding=1) == 1:
    #             raise ValidationError(_('The total number of selections cannot exceed the total number of packaging'))

    @api.depends('picking_list_lines.quantity')
    def _compute_select_qty(self):
        for rec in self:
            rec.select_qty = sum(rec.picking_list_lines.mapped('quantity'))

    @api.depends('mix_box_id_ids', 'mix_box_id_ids.sequence')
    def _compute_mix_box_id_ids(self):
        for rec in self:
            rec.mix = False
            rec.display_mix = False
            if rec.mix_box_id_ids:
                rec.mix = True
                rec.display_mix = _('Mix') + str(rec.mix_box_id_ids[0].sequence)

    def _compute_display_name(self):
        for rec in self:
            if self.env.context.get('MO'):
                rec.display_name = rec.shipping_instructions_line_id.sale_order_id.name
            else:
                rec.display_name = rec.name

    def action_open_lot_datecode_summary(self):
        self.ensure_one()
        return {
            'name': 'Lot Datecode Summary',
            'res_model': 's.lot.datecode.summary.wiz',
            'view_mode': 'form',
            'type': 'ir.actions.act_window',
            'target': 'new',
            'context': {
                'default_package_id': self.id
            }
        }

    # def action_open_lot_datecode_summary(self):
    #     """
    #     按照当前记录 product_id 查找所有库存数量大于 0 的批次，
    #     按照批次的 s_week_of_year 字段分组，每个组生成一条 wizard 记录，
    #     """
    #     Quant = self.env['stock.quant']
    #     if self.ttl_qty == 0:
    #         raise ValidationError(_('Please complete the filling of the required quantity first'))
    #     stock_locations = self.env['stock.location'].search([('name', '=', '成品仓')])
    #     if not stock_locations:
    #         raise ValidationError(_("I can't find the inventory location >> Finished product warehouse"))
    #
    #     product_ids = self.product_id.s_base_id.product_tmpl_ids.product_variant_ids
    #     product_ids |= self.product_id
    #     # 获取当前产品在成品仓的 quants
    #     quants = Quant.search([
    #         ('product_id', 'in', product_ids.ids),
    #         ('available_quantity', '>', 0),
    #         ('location_id', 'in', stock_locations.ids)
    #     ])
    #
    #     lots = quants.mapped('lot_id')
    #     if not lots:
    #         raise ValidationError(_('In no stock!'))
    #
    #     # 根据批次的 s_week_of_year 分组
    #     grouped = {}
    #     for lot in lots:
    #         week = lot.s_week_of_year or ''
    #         grouped.setdefault(week, [])
    #         grouped[week].append(lot.id)
    #
    #     wizard_ids = []
    #     # 针对每个分组生成一条 wizard 记录
    #     remain_quantity = self.ttl_qty + self.free_sample
    #     for week, lot_id_list in grouped.items():
    #         ttl_qty = sum(quants.filtered(lambda q: q.lot_id and q.lot_id.s_week_of_year == week).mapped('available_quantity'))
    #         filtered_pick_lists = self.pd_id.pick_lists.filtered(
    #             lambda pl: pl.product_id.id == self.product_id.id and pl.id != self.id
    #         )
    #         result = []
    #         for pick_list in filtered_pick_lists:
    #             if pick_list.datecode_line_ids:
    #                 filtered_datecode_lines = pick_list.datecode_line_ids.filtered(lambda dc: dc.datecode == week)
    #                 result.extend(filtered_datecode_lines)
    #         total_qty = sum(line.qty for line in result)
    #         date_code_remain = ttl_qty - total_qty
    #         if date_code_remain <= 0:
    #             continue
    #         if date_code_remain > remain_quantity:
    #             qty = remain_quantity
    #             remain_quantity = 0
    #         else:
    #             qty = date_code_remain
    #             remain_quantity -= qty
    #         wiz_rec = self.env['s.lot.datecode.summary.wiz'].sudo().create({
    #             'datecode': week if week else 'UnKnow',
    #             'ttl_qty': date_code_remain,
    #             'lot_ids': [(6, 0, lot_id_list)],
    #             'package_id': self.id,
    #             'qty': qty,
    #         })
    #         wizard_ids.append(wiz_rec.id)
    #
    #     return {
    #         'name': 'Lot Datecode Summary',
    #         'domain': [('id', 'in', wizard_ids)],
    #         'res_model': 's.lot.datecode.summary.wiz',
    #         'view_mode': 'tree,form',
    #         'type': 'ir.actions.act_window',
    #         'target': 'new',
    #
    #     }

    @api.onchange('picking_list_lines.quantity', 'picking_list_lines')
    def compute_remain_qty(self):
        for rec in self:
            delivery_qty = rec.delivery_qty
            quantity = sum([pl.quantity for pl in self.picking_list_lines])
            rec.remain_available_quantity = delivery_qty - quantity
            if rec.remain_available_quantity < 0:
                raise ValidationError(_('The maximum quantity cannot exceed the remaining quantity'))

    @api.depends('package', 'qty', 'g_weight')
    def _compute_tt_g_weight(self):
        for rec in self:
            rec.tt_g_weight = rec.package * rec.g_weight
            rec.tt_n_weight = rec.package * rec.n_weight

    @api.depends('n_weight')
    def _compute_g_weight(self):
        for rec in self:
            rec.g_weight = rec.n_weight + 0.5

    @api.constrains('qty', 'package', 'shipping_instructions_line_id')
    def _check_package_limit(self):
        for rec in self:
            if rec.shipping_instructions_line_id:
                picking_list_lines = rec.shipping_instructions_line_id.package_ids or self.search([
                    ('shipping_instructions_line_id', '=', rec.shipping_instructions_line_id.id)
                ])
                total_packaged = sum(line.package * line.qty for line in picking_list_lines)
                if total_packaged > rec.shipping_instructions_line_id.delivery_qty:
                    raise ValidationError(_(
                        "The current '%s' total package quantity (%s) exceeds the delivered quantity (%s)."
                    ) % (rec.shipping_instructions_line_id.display_name, total_packaged,
                         rec.shipping_instructions_line_id.delivery_qty))

    @api.depends('qty', 'package')
    def _compute_ttl_package(self):
        for rec in self:
            rec.ttl_qty = rec.package * rec.qty
            rec.ttl_value = rec.ttl_qty * rec.unit_price

    def action_assign_serial(self):
        """ Opens a wizard to assign SN's name on each move lines.
        """
        self.ensure_one()
        action = self.env["ir.actions.actions"]._for_xml_id("sb_sale.sb_picking_list_act_window")
        action['res_id'] = self.id
        return action

    def onchange_quantity(self, quant):
        quantity = 0
        # 所有未完成的包装单
        all_pds = self.env['sb.packing.delivery'].sudo().search([('state', 'in', ['pack', 'confirming']), ('id', '!=', self.pd_id.id)])
        all_lines = self.pd_id.picking_list_lines
        for pd in all_pds:
            all_lines |= pd.picking_list_lines

        available_quantity = quant.available_quantity - sum([al.quantity for al in all_lines if
                                                             al.product_id.id == self.product_id.id
                                                             and al.quant_id == quant
                                                             and self.ids[0] != al.picking_list_id.id])

        remain_available_quantity = self.remain_available_quantity
        # 库存可用=剩余
        if available_quantity > 0 and available_quantity == remain_available_quantity:
            quantity = remain_available_quantity
        # 库存可用>剩余
        elif available_quantity > 0 and available_quantity > remain_available_quantity:
            quantity = remain_available_quantity
        # 库存可用 < 剩余
        elif available_quantity > 0 and available_quantity < remain_available_quantity:
            quantity = available_quantity
        return quantity, available_quantity


class SbPD(models.Model):
    _name = 'sb.packing.delivery'
    _description = 'packing & delivery'
    _inherit = ['portal.mixin', 'mail.thread.main.attachment', 'mail.activity.mixin']

    name = fields.Char()
    ship_instructions_id = fields.Many2one('ship.instructions', ondelete='restrict', string='Instructions ID')
    tr_no = fields.Char(string='TR No.')
    shipping_instructions_line_ids = fields.Many2many('ship.instructions.line',
                                                      related='ship_instructions_id.unique_line_ids', string='PL')
    sl_line_ids = fields.One2many('ship.instructions.line',
                                  related='ship_instructions_id.line_ids', string='Instruction Line')
    pick_lists = fields.One2many('sb.picking.list', 'pd_id', string='Pick Lists')
    state = fields.Selection([
        ('cancel', 'Cancel'),
        ('pack', 'Packing'),
        ('confirming', 'Confirming'),
        ('logistics', 'Logistics'),
        ('done', 'Done'),
    ], default='pack', tracking=True, string='State')
    shipping_method = fields.Many2one('s.shipping.method', string='Shipping Method',
                                      related='ship_instructions_id.shipping_method')
    bank_num = fields.Char(string="Account No.", related='ship_instructions_id.bank_num')
    declaration_form_no = fields.Char(string='Export declaration No.')
    currency = fields.Many2one('res.currency', string='Currency', related='ship_instructions_id.currency_id', store=True)
    freight_charges = fields.Float(string='Freight Charges')
    forwarder = fields.Char(string='Forwarder')
    partner = fields.Many2one('res.partner', string='Customer', related='ship_instructions_id.partner_id', store=True)
    jove_code = fields.Char(related='partner.s_regular_code')
    shipping_id = fields.Many2one('res.partner', string='Shipping Info', related='ship_instructions_id.shipping_id')
    delivery_address = fields.Text(string='Delivery Address', related='ship_instructions_id.delivery_address')
    full_delivery_address = fields.Char(string='Full Address', compute='_compute_full_delivery_address')
    carton_number = fields.Integer(string='Cartons(Box)', compute='_compute_package')
    nw_ttl = fields.Float(string='N.W. TTL(Kg)', compute='_compute_package', digits=(12, 2))
    gw_ttl = fields.Float(string='G.W. TTL(Kg)', compute='_compute_package', digits=(12, 2))
    incoterm = fields.Many2one('account.incoterms', string='Incoterm', related='ship_instructions_id.incoterm')
    shipping_date = fields.Date(string='Shipping Date', related='ship_instructions_id.shipping_date', store=True)
    invoice_ids = fields.Many2many('account.move', string='Invoices', related='ship_instructions_id.invoice_ids')
    account_lines = fields.Many2many('account.move.line', string='Account Lines', compute='_compute_account_lines')
    amount_ttl = fields.Monetary(string='Amount TTL', compute='_compute_account_lines', currency_field='currency')
    actual_shipping_date = fields.Date(string='Actual Shipping')
    date_arrival = fields.Date('Date of Arrival')
    picking_list_lines = fields.One2many('sb.picking.list.line', 'pd_id', string='Stock')
    pallet = fields.Boolean(string='Use Pallet', default=False)
    sb_pallet_qty = fields.Integer(string="Export pallets qty")
    sb_pallet_length = fields.Float(string="Pallet Length (cm)")
    sb_pallet_width = fields.Float(string="Pallet Width (cm)")
    sb_pallet_height = fields.Float(string="Pallet Height (cm)")
    note = fields.Text(string='Note', related='ship_instructions_id.note')
    document_ids = fields.One2many('documents.document', 'res_id', string='Documents',
                                   domain="[('res_model', '=', 'sb.packing.delivery'), ('res_id', '=', id)]")
    sp_id = fields.Many2one('stock.picking', string='Delivery No.', related='ship_instructions_id.instructor_sp')
    delivery_state = fields.Selection([
        ('unconfirmed', 'Unconfirmed'),
        ('waiting', 'Waiting Delivery'),
        ('done', 'Done'),
    ], string='Delivery State', default='unconfirmed')
    mix_package = fields.Boolean('Mix Package', default=False)
    mix_package_ids = fields.One2many('sb.wrapped.in.box', 'pd_id', string='Mix Package')

    @api.depends('shipping_id')
    def _compute_full_delivery_address(self):
        for record in self:
            if record.shipping_id:
                partner = record.shipping_id
                company_name = partner.parent_id.name if partner.parent_id else partner.name
                street = partner.street or ''
                city_state_zip = f"{partner.city or ''} {partner.state_id.name or ''} {partner.zip or ''}"
                country = partner.country_id.name or ''
                contact_person = partner.name if partner.parent_id else ''
                phone = partner.phone or ''
                email = partner.email or ''

                record.full_delivery_address = (
                    f"{company_name}\n"
                    f"{street}\n"
                    f"{city_state_zip}\n"
                    f"{country}\n"
                    f"{contact_person}\n"
                    f"{phone}\n"
                    f"{email}"
                )
            else:
                record.full_delivery_address = ''

    @api.onchange('tr_no')
    def onchange_tr_no(self):
        if self.tr_no and self.sp_id:
            self.sp_id.delivery_no = self.tr_no

    def action_view_stock_picking(self):
        if self.sp_id:
            return {
                'type': 'ir.actions.act_window',
                'name': _('Delivery'),
                'res_model': 'stock.picking',
                'view_mode': 'form',
                'res_id': self.sp_id.id,
                'target': 'current',
            }
        else:
            return {
                'type': 'ir.actions.client',
                'tag': 'display_notification',
                'params': {
                    'title': _('Warning'),
                    'message': _('No related stock picking found.'),
                    'sticky': False,
                }
            }

    def action_view_documents(self):
        self.ensure_one()
        return {
            'name': 'Documents',
            'type': 'ir.actions.act_window',
            'res_model': 'documents.document',
            'view_mode': 'kanban,tree,form',
            'domain': [
                ('res_model', '=', 'sb.packing.delivery'),
                ('res_id', '=', self.id),
            ],
            'context': {
                'default_res_model': 'sb.packing.delivery',
                'default_res_id': self.id,
                'create': False,
                'searchpanel_default_folder_id': self.env.ref('sb_sale.sb_packing_folder').id,
            },
            'target': 'current',
        }

    def action_open_document_upload_wizard(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Upload Document',
            'res_model': 'document.upload.wizard',
            'view_mode': 'form',
            'target': 'new',
            'context': {
                'default_res_model': 'sb.packing.delivery',
                'default_res_id': self.id,
                'default_folder_id': self.env.ref('sb_sale.sb_packing_folder').id,
                'partner_id': self.partner.id,
            },
        }

    @api.depends('invoice_ids')
    def _compute_account_lines(self):
        for rec in self:
            rec.account_lines = rec.invoice_ids.mapped('line_ids').filtered(lambda line: line.product_id.detailed_type == 'product')
            rec.amount_ttl = sum(line.price_subtotal for line in rec.account_lines)

    @api.depends('pick_lists', 'pick_lists.package', 'pick_lists.n_weight', 'pick_lists.g_weight',
                 'mix_package_ids', 'mix_package_ids.n_weight', 'mix_package_ids.g_weight')
    def _compute_package(self):
        for rec in self:
            carton = len(rec.mix_package_ids) + sum(
                rec.pick_lists.filtered(lambda pl: pl not in rec.mix_package_ids.mapped('pl_ids')).mapped('package')
            )
            nw_ttl = sum(
                line.n_weight * line.package for line in rec.pick_lists
                if line not in rec.mix_package_ids.mapped('pl_ids')
            ) + sum(
                line.n_weight for line in rec.mix_package_ids
            )
            gw_ttl = sum(
                line.g_weight * line.package for line in rec.pick_lists
                if line not in rec.mix_package_ids.mapped('pl_ids')
            ) + sum(
                line.g_weight for line in rec.mix_package_ids
            )
            rec.carton_number = carton
            rec.nw_ttl = nw_ttl
            rec.gw_ttl = gw_ttl

    def button_return(self):
        self.state = 'confirming'
        self.ship_instructions_id.state = 'command'

    def pack_done(self):
        for rec in self:
            # 1. 基础校验
            if not rec.pick_lists or sum(rec.pick_lists.mapped('package')) == 0:
                raise ValidationError(_('Please complete the packaging first, or what you need is to send it back to the market!!!'))
            # if not rec.actual_shipping_date:
            #     raise ValidationError(_("Please input the Actual Shipping Date first."))
            for line in rec.pick_lists:
                if not line.note:
                    raise ValidationError(_('Please input the date code for all packages!!!'))
                # 重量檢查
                if (line.n_weight <= 0 or line.g_weight <= 0) and line.package > 0:
                    raise ValidationError(_('Please input the N.W. and G.W. for all packages!!!'))
                rounding = line.product_id.uom_id.rounding or 0.0001  # 避免除以零
                # 數量檢查
                picking_qty = sum(line.picking_list_lines.mapped('quantity'))
                pack_qty = line.free_sample + line.ttl_qty

                if float_compare(pack_qty, picking_qty, precision_rounding=rounding) == 1:
                    raise ValidationError(_('The selected inventory quantity cannot be less than the packing quantity!'))

            # 2. 切换状态
            rec.state = 'confirming'
            rec.ship_instructions_id.state = 'shipping_confirmation'

            # 3. 获取频道和创建人
            channel = self.env['discuss.channel'].sudo().search([('uuid', '=', 'ySHCgPsWJM')], limit=1)
            creator = rec.ship_instructions_id.create_uid
            partner = creator.partner_id
            # 构造跳转 URL（无需指定 action/menu，Odoo 会使用默认视图）
            url = f"/web#id={rec.ship_instructions_id.id}&model={rec.ship_instructions_id._name}&view_type=form"

            # 4. 完全匹配数量 → 自动确认发货
            if rec.ship_instructions_id.ttl_instructions == sum(rec.pick_lists.mapped('ttl_qty')):
                # 4.1 在 chatter 留言
                rec.ship_instructions_id.sudo().message_post(
                    body=_('Auto Confirm of Shipment.'),
                    author_id=self.env.user.partner_id.id,
                )
                # 4.2 真正发货
                rec.ship_instructions_id.sudo().confirm_shipment(admin=True)
                try:
                    rec.sudo().ship_instructions_id.instructor_sp.button_validate()
                except Exception:
                    return {
                        'type': 'ir.actions.act_window',
                        'name': _('Delivery'),
                        'res_model': 'stock.picking',
                        'view_mode': 'form',
                        'res_id': rec.ship_instructions_id.instructor_sp.id,
                    }

                # 4.3 频道通知 — 出货成功
                if channel and partner:
                    msg = Markup(_(
                        "<div style='color:green;'>✅ <b>Shipment Completed</b></div>"
                        "<div><b>%(name)s</b> has been successfully shipped.</div>"
                        "<div><span class='o_mail_mentions' data-oe-id='%(pid)d' data-oe-model='res.partner'>@%(pname)s</span>, please take note.</div>"
                        "<div><a href='%(url)s' target='_blank'>📎 Click here to view the instruction</a></div>"
                    ) % {
                                     'name': rec.ship_instructions_id.name,
                                     'pid': partner.id,
                                     'pname': partner.name,
                                     'url': url,
                                 })
                    channel.sudo().message_post(
                        body=msg,
                        subject=_("Shipment Completed"),
                        subtype_xmlid='mail.mt_comment',
                        message_type="comment",
                        author_id=self.env.user.partner_id.id,
                    )

                return {
                    'type': 'ir.actions.client',
                    'tag': 'display_notification',
                    'params': {
                        'title': _('Success'),
                        'type': 'success',
                        'message': _('Completed shipment.'),
                        'sticky': False,
                        'next': {'type': 'ir.actions.act_window_close'},
                    }
                }

            # 5. 数量不一致 → 警告并通知
            else:
                if channel and partner:
                    msg = Markup(_(
                        "<div style='color:orange;'>⚠️ <b>Quantity Mismatch</b></div>"
                        "<div>Shipment for <b>%(name)s</b> was not completed due to inconsistent packaging quantities.</div>"
                        "<div><span class='o_mail_mentions' data-oe-id='%(pid)d' data-oe-model='res.partner'>@%(pname)s</span>, please verify.</div>"
                        "<div><a href='%(url)s' target='_blank'>📎 Click here to view the instruction</a></div>"
                    ) % {
                                     'name': rec.ship_instructions_id.name,
                                     'pid': partner.id,
                                     'pname': partner.name,
                                     'url': url,
                                 })
                    channel.sudo().message_post(
                        body=msg,
                        subject=_("Quantity Mismatch"),
                        subtype_xmlid='mail.mt_comment',
                        message_type="comment",
                        author_id=self.env.user.partner_id.id,
                    )

                return {
                    'type': 'ir.actions.client',
                    'tag': 'display_notification',
                    'params': {
                        'title': _('Warning'),
                        'type': 'warning',
                        'message': _('The quantities are inconsistent and need to be confirmed by the market.'),
                        'sticky': False,
                        'next': {'type': 'ir.actions.act_window_close'},
                    }
                }

    def export_shipping_instructions(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_url',
            'url': f'/export_shipping_instructions/{self.id}',
            'target': 'self',
        }

    def button_done(self):
        for rec in self:
            if rec.declaration_form_no == '':
                raise ValidationError(_('Please input the Declaration Form No. first!!!'))
            if not rec.date_arrival:
                raise ValidationError(_('Please input the Date of arrival.'))
            rec.state = 'done'

    def print_invoice(self):
        """打印多张发票"""
        if not self.invoice_ids:
            raise ValidationError(_("Please create the Invoice first."))
        if not self.actual_shipping_date:
            raise ValidationError(_("Please input the Actual Shipping Date first."))
        if self.ship_instructions_id.currency_id.name == 'THB':
            report_external_id = 'sb_sale.sb_inv_picking_list_report_action'
        else:
            report_external_id = 'sb_sale.report_sb_sinv'
        return self.env.ref(report_external_id).with_context(lang='en_US').report_action(self.invoice_ids)

    def print_pick_list(self):
        """Print on button click"""
        if not self.actual_shipping_date:
            raise ValidationError(_("Please input the Actual Shipping Date first."))
        if not self.pick_lists:
            raise ValidationError(_("Please create the Package List first."))
        else:
            if self.ship_instructions_id.currency_id.name == 'THB':
                report_external_id = 'sb_sale.sb_delivery_picking_list_report_action'
            else:
                report_external_id = 'sb_sale.sb_picking_list_report_action'
            return self.env.ref(report_external_id).with_context(lang='en_US').report_action(self.ship_instructions_id.id)
